
package com.hulk.dryad.manage.boot.logger;

import cn.ms.sequence.IdWorker;
import com.hulk.dryad.common.annotation.PrintlnLog;
import com.hulk.dryad.common.util.ClassUtils;
import com.hulk.dryad.common.util.JacksonUtil;
import com.hulk.dryad.common.util.SecurityUtils;
import com.hulk.dryad.common.util.WebUtils;
import com.hulk.dryad.manage.boot.launch.DryadLogLevel;

import com.hulk.dryad.manage.framework.tenant.TenantContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dreamlu.mica.core.utils.StringUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

/**
 * Spring boot 控制器 请求日志，方便代码调试
 *
 * @author hulk
 */
@Slf4j
@Aspect
@RequiredArgsConstructor
public class RequestLogAspect {

	private final DryadRequestLogProperties properties;

	/**
	 * 以自定义 @PrintlnLog 注解作为切面入口
	 */
	@Pointcut("@annotation(com.hulk.dryad.common.annotation.PrintlnLog) && " +
			"(@within(org.springframework.stereotype.Controller) || " +
			"@within(org.springframework.web.bind.annotation.RestController))")
	public void PrintlnLog() {
	}

	/**
	 * AOP 环切 控制器 R 返回值
	 *
	 * @param point JoinPoint
	 * @return Object
	 * @throws Throwable 异常
	 */
	@Around("PrintlnLog()")
	public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
		DryadLogLevel level = properties.getRequest().getLevel();
		// 不打印日志，直接返回
		if (DryadLogLevel.NONE == level) {
			return point.proceed();
		}
		Long workId = IdWorker.getId();

		HttpServletRequest request = WebUtils.getRequest();
		String requestURI = request.getRequestURI();
		String requestMethod = request.getMethod();

		// 构建成一条长 日志，避免并发下日志错乱
		StringBuilder beforeReqLog = new StringBuilder(300);
		// 日志参数
		List<Object> beforeReqArgs = new ArrayList<>();
		beforeReqLog.append("\n\n================  Request Start  ================\n");
		// 打印路由
		beforeReqLog.append("===> {}: {}");
		beforeReqArgs.add(requestMethod);
		beforeReqArgs.add(requestURI);

		// 打印用户信息
		logUser(workId,point, beforeReqLog, beforeReqArgs);
		// 打印请求参数
		logIngArgs(point,request, beforeReqLog, beforeReqArgs);
		// 打印请求 headers
		logIngHeaders(request, beforeReqLog, beforeReqArgs, level);
		beforeReqLog.append("================   Request End   ================\n");

		// 打印执行时间
		long startNs = Instant.now().getNano();
		log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
		// aop 执行后的日志
		StringBuilder afterReqLog = new StringBuilder(250);
		// 日志参数
		List<Object> afterReqArgs = new ArrayList<>();
		afterReqLog.append("\n\n================  Response Start  ================\n");
		try {
			  Object result = point.proceed();
			  if (DryadLogLevel.BODY.lte(level)) {
				// 打印返回结构体
				afterReqLog.append("===WorkId: [{}], Result: [{}]===\n");
				afterReqArgs.add(workId);
				afterReqArgs.add(JacksonUtil.toJson(result));
			  }
      return result;
		} finally {
			String tookTime = StringUtil.format(Instant.now().getNano() - startNs);
			afterReqLog.append("<=== {}: {} ({} ms)\n");
			afterReqArgs.add(requestMethod);
			afterReqArgs.add(requestURI);
			afterReqArgs.add(tookTime);
			afterReqLog.append("================   Response End   ================\n");
			log.info(afterReqLog.toString(), afterReqArgs.toArray());
		}
	}

	private void logUser(Long workId,ProceedingJoinPoint point,StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
		beforeReqLog.append("===WorkId: [{}],PrintlnLog value: [{}],  Username: [{}], TenantId: [{}]===\n");
		beforeReqArgs.add(workId);
		MethodSignature ms = (MethodSignature) point.getSignature();
		Method method = ms.getMethod();
		String desc = method.getAnnotation(PrintlnLog.class).value();
		beforeReqArgs.add(desc);
		beforeReqArgs.add(getUsername());
		beforeReqArgs.add(getTenantId());

	}
	/**
	 * 记录请求参数
	 *
	 * @param point         ProceedingJoinPoint
	 * @param request       HttpServletRequest
	 * @param beforeReqLog  StringBuilder
	 * @param beforeReqArgs beforeReqArgs
	 */
	private static void logIngArgs(ProceedingJoinPoint point,
								   HttpServletRequest request,
								   StringBuilder beforeReqLog,
								   List<Object> beforeReqArgs) {
		MethodSignature ms = (MethodSignature) point.getSignature();
		Method method = ms.getMethod();
		Object[] args = point.getArgs();
		Map<String, String[]> paramMap = request.getParameterMap();
		paramMap.forEach((name, value) -> {
			beforeReqLog.append("===Parameters===  {}: {}\n");
			beforeReqArgs.add(name);
			beforeReqArgs.add(StringUtils.arrayToCommaDelimitedString(value));
		});
		for (int i = 0; i < args.length; i++) {
			// 读取方法参数
			MethodParameter methodParam = ClassUtils.getMethodParameter(method, i);
			PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
			if (pathVariable != null) {
				beforeReqLog.append("===PathVariable===  {}: {}\n");
				beforeReqArgs.add(pathVariable.name());
				beforeReqArgs.add(args[i]);
			}
		}
		// 一次请求只能有一个 request body
		Object requestBodyValue = null;
		for (int i = 0; i < args.length; i++) {
			// 读取方法参数
			MethodParameter methodParam = ClassUtils.getMethodParameter(method, i);
			RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
			// 如果是body的json则是对象
			if (requestBody != null) {
				requestBodyValue = args[i];
				break;
			}
		}
		if (requestBodyValue != null) {
			beforeReqLog.append("===RequestBody==  {}\n");
			beforeReqArgs.add(JacksonUtil.toJson(requestBodyValue));
		}


	}

	/**
	 * 记录请求头
	 *
	 * @param request       HttpServletRequest
	 * @param beforeReqLog  StringBuilder
	 * @param beforeReqArgs beforeReqArgs
	 */
	private static void logIngHeaders(HttpServletRequest request,
									  StringBuilder beforeReqLog, List<Object> beforeReqArgs,  DryadLogLevel level) {
    if (DryadLogLevel.HEADERS.lte(level)) {
		  // 打印请求头
		  Enumeration<String> headers = request.getHeaderNames();
		  while (headers.hasMoreElements()) {
			String headerName = headers.nextElement();
			String headerValue = request.getHeader(headerName);
			beforeReqLog.append("===Headers===  {}: {}\n");
			beforeReqArgs.add(headerName);
			beforeReqArgs.add(headerValue);
		  }
		}
	}

	private static String getUsername() {
		Authentication authentication = SecurityUtils.getAuthentication();
		if (authentication == null) {
			return null;
		}
		return authentication.getName();
	}

	private  static String getTenantId() {
		return TenantContextHolder.getTenantId();
	}
}
